写给设计师的 Processing 编程指南(12) - 类与对象
类是什么?对象是什么?
类是面向对象编程中才会有的概念。不要把它想得过于高深。其实前面的很多例子,你已经不知不觉地使用了类,只是没有去深入。
那类和对象到底是什么?
简单地说。类是用于描述某类事物的属性和特征,它是“抽象”的。对象则是类的一个实体,它是“具象”的。
打个比方。“国家”如果作为类,那“中国”就是这个类的对象。“昆虫”如果作为类,那“蝴蝶”就是这个类的对象。
我们给类下定义的时候,便会把一些东西打包起来。在程序中,它可以是变量,也可以是函数。当把类实例化时,生成的对象都会包含这些特征。类是为了模块化,为了偷懒,为了提高效率而产生的。如果不想重复劳动,就可以多使用类。
相信现在你已经对类有一个基本概念了。下面看具体的实例
类的语法
class 类名{
成员变量
构造函数
成员函数
}
首先,在开头需要写上关键字 class。
接着给类取一个名字。类的名称一般首字母要大写,这样可以和其他数据类型区分开来。像之前提到的 String 类型,它其实就是程序中定义好的类。所以与 int,float,boolean 这些基本数据类型不同,首字母用了大写。除了这点之外,它与一般的变量名,函数名的命名规则是一致的。尽量简洁易懂,并且不要与已有的函数名,变量名重复。
后面再写大括号,里面就是类的常见组成部分。
成员变量:作为类当中的变量,用于存放数据。
构造函数:用于初始化对象
成员函数:作为类当中的函数,实现特定功能。
当然,这些不是类里面必须有的。根据不同的需求,可以有不同的写法。下面先抽丝剥茧,从最简单的开始。构建一个“ class 块 ”。
创建 class 块
class 块是一个空壳,里面不包含任何内容。在Processing中可以允许这样写。
代码示例(12-1):
MyClass mc;
void setup(){
mc = new MyClass();
}
void draw(){
}
class MyClass{
}
代码说明:
class 和 setup 函数,draw 函数是“同级”。既可以写在 setup 函数前,也能写在 draw 函数之后。但一般的写法是放在 draw 函数下方。
第一行代码声明了一个名叫 mc 的类对象。这种格式写法与声明 int ,float 等类型是一致的。
setup 函数中的 new 语句作用是将 mc 对象初始化。这步不能忽略,只有经过初始化对象才可使用。
通过这种方法,就创建了一个有关类的空壳。类除了可以写在 draw 函数下方。还可以写在新的标签上。当你的文件规模越来越大的时候,这样可以更好地管理。方法是点击文档名标签的右方的一个朝下的三角符号,选新建标签
输入标签名,即可创建完成(标签的名称不一定与类名一致)
之后就能在里面写类
创建的标签,都会以 pde 的格式保存在工程文件的同级目录下。程序在运行时,会自动把这些文件引入到一个工程中。
类的应用-构建人物信息库
下面开始小试牛刀,会开始在类中使用成员变量,解决一些实际问题。上一章有关数组的某个示例,我们使用了三个不同类型的数组来储存人物信息。
String[] name;
boolean[] gender;
float[] heights;
int[] age;
void setup() {
name = new String[]{"Mike", "Jake", "Kate"};
gender = new boolean[]{true, true, false};
heights = new float[]{0.98, 1.34, 1.7};
age = new int[]{5, 10, 18};
}
下图可以表示数据的打包情况,它是以数组为单位对信息分开储存
这样虽然可以达到储存的目的,但显然很不直观。对于名字,性别,身高,年龄这些属性,最终都是依附于某个个体的,以人为单位来会更直观。但由于程序本身没有提供这类复合的数据类型来表示“人”。所以这时候类就能派上用场了,我们可以用新的方式组织这些数据。重组后有点像下图。
Person 将作为一个类,来打包这些数据。
下面用一个实例来了解“Person”是如何实现的
代码示例(12-2):
Person mike;
void setup(){
mike = new Person();
mike.age = 10;
mike.gender = false;
mike.heights = 1.8;
println(mike.age);
}
class Person{
boolean gender;
float heights;
int age;
}
代码说明:
和普通的变量一样,类中的成员变量只是作为容器。一旦创建就能进行读取和写入。
浮点变量用“ heights ”,而不用“ height ” 是为了不与默认的 height 变量重名。
通过[ 类名 + “.” + 变量名 ] ,就能访问类中的成员
对象名是以人名起的,但为了方便调取信息,类中仍保留一个String类型来储存名字
类的应用-粒子系统
打造“粒子”-使用成员变量
熟悉了成员变量的用法,就能进入更有趣的部分了-用类去写粒子系统。粒子系统是一个概念,没有明确的定义。它可用于描述粒子的状态和运动。常被用来模拟自然形态,如雨雪,河流,烟尘,瀑布,火焰等。天空的鸟群,水中的鱼群,射击游戏中的子弹,爆炸都能用它模拟。
当然,从广义上讲,任何图像其实都可以看作粒子。像电子屏幕上显示的图像,都是由一堆粒子(像素)组成的。它们有固定的位置,色值,大小。下面先用类,来模拟粒子的一些基本属性。
代码示例(12-3):
void setup() {
size(700, 700);
p = new Particle();
p.col = color(202, 31, 201);
p.x = 350;
p.y = 350;
p.r = 200;
}
void draw() {
background(33, 48, 64);
fill(p.col);
ellipse(p.x, p.y, p.r * 2, p.r * 2);
}
class Particle {
color col;
float x, y;
float r;
}
代码说明:
从结果上看,只是在屏幕的特定位置用特定颜色画了一个圆,但数据的组织结构已经发生变化了。这是一个简化版的粒子系统。在类中创建了四个成员变量,来代表粒子的横纵坐标,大小以及颜色。
打造“粒子”-使用构造函数
接下来再对类的概念做一些拓展。在 Particle 类中加入构造函数。
构造函数的作用是对某些变量值进行初始化。我们可以把一些需要在前期就设定好参数的变量,写进构造函数中。
代码示例(12-4):
Particle p;
void setup() {
size(700, 700);
p = new Particle();
}
void draw() {
background(33, 48, 64);
fill(p.col);
noStroke();
ellipse(p.x, p.y, p.r, p.r);
}
class Particle {
color col;
float x, y;
float r;
Particle() {
col = int(random(0, 255));
x = random(width);
y = random(height);
r = random(100, 500);
}
}
代码说明:
构造函数的格式是类名后加小括号,大括号。这与一般定义函数的写法非常接近,只是前面无需写 void
setup 中的 “ new ”,作用是对对象进行初始化。一旦使用这个命令,构造函数便会自动执行。因此每次打开程序,都会得到不一样的结果。假如我们希望多次调用构造函数。就可以使用 “ new ”。在 keyPressed 事件中加上如下代码,便能通过按键重设粒子的参数。
示例:
void keyPressed() {
p = new Particle();
}
打造“粒子”-构造函数传入参数
构造函数毕竟是函数。所以也允许传入多个参数。
代码示例(12-5):
Particle p;
void setup() {
size(700, 700);
p = new Particle(350, 350, 400, color(255, 200, 0));
}
void draw() {
background(33, 48, 64);
fill(p.col);
noStroke();
ellipse(p.x, p.y, p.r, p.r);
}
class Particle {
color col;
float x, y;
float r;
Particle(float x_, float y_, float r_, color col_) {
x = x_;
y = y_;
r = r_;
col = col_;
}
}
代码说明:
构造函数小括号中的参数被称为形式参数,它不是实际存在的变量,只起传递的作用。形式参数的名称后加下划线没有特殊的含义,它只是充当字母字符,是一种比较常规的写法,方便赋值时逐一对应。
在使用 new 对对象进行初始化时,填写参数的个数和类型必须与构造函数一致,否则会出错
另外,构造函数也支持重载。可以定义多个构造函数。根据构造函数参数的个数和类型来决定初始化时调用哪个。
类示例:
class Particle {
color col;
float x, y;
float r;
Particle() {
col = int(random(0, 255));
x = random(width);
y = random(height);
r = random(100, 500);
}
Particle(float x_, float y_, float r_, color col_) {
x = x_;
y = y_;
r = r_;
col = col_;
}
}
打造“粒子”-使用成员函数
最后介绍的是成员函数。顾名思义它是被包含在类中的函数。通过[ 对象名 + “.” + 函数名 ],就能在外部访问。
代码示例(12-6):
Particle p;
void setup() {
size(700, 700);
p = new Particle(350, 350, 400, color(255, 200, 0));
}
void draw() {
background(33, 48, 64);
p.randomMove();
fill(p.col);
noStroke();
ellipse(p.x, p.y, p.r, p.r);
}
class Particle {
color col;
float x, y;
float r;
Particle(float x_, float y_, float r_, color col_) {
x = x_;
y = y_;
r = r_;
col = col_;
}
void randomMove() {
x+= random(-10, 10);
y+= random(-10, 10);
}
}
类的综合应用-粒子系统(数组)
使用数组
前面介绍了成员变量,构造函数,成员函数。使单个粒子有了属性和运动状态。下面将通过 数组 来创建一群粒子,打造一个跟随鼠标运动的粒子系统。
代码示例(12-7):
Particle[] circles;
void setup(){
size(700,700);
circles = new Particle[300];
for(int i = 0;i < circles.length;i++){
circles[i] = new Particle(random(width),random(height));
}
}
void draw(){
background(244,213,63);
noStroke();
for(int i = 0;i < circles.length;i++){
circles[i].randomMove();
circles[i].follow();
circles[i].draw();
}
}
class Particle{
float x,y;
int colorStyle;
float ratio;
float r;
Particle(float x_,float y_){
x = x_;
y = y_;
r = random(5,20);
colorStyle = int(random(4));
ratio = random(0.005,0.05);
}
void randomMove(){
x = x + random(-5,5);
y = y + random(-5,5);
}
void follow(){
x = x + (mouseX - x) * ratio;
y = y + (mouseY - y) * ratio;
}
void draw(){
float alpha = 255;
if(colorStyle == 0){
// 红
fill(232,8,80,alpha);
}else if(colorStyle == 1){
// 紫色
fill(104,8,240,alpha);
}else if(colorStyle == 2){
// 黑
fill(0,alpha);
}else if(colorStyle == 3){
// 白
fill(255,alpha);
}
ellipse(x,y,r * 2,r * 2);
}
};
运行效果:
代码说明:
除了 int,float 这些基本数据类型可以使用数组。类也可以被数组化。
成员函数 follow 实现了跟随效果。用到了一个经典表达式 A = A + (B - A) * ratio。其中 A 代表当前点坐标,B 代表目标点坐标,ratio 代表每次逼近的比率。在示例中 A 表示粒子当前的坐标位置,B 表示鼠标当前的坐标位置。B - A 计算得到的是两者间相差的距离。这段距离之后乘以一个参数,得出的数值就是此段距离的几分之几。每调用一次函数,A 都会持续加上这段距离差的几分之几,因而也越来越逼近了。另外,由于程序默认帧率是非常高的,此函数每秒执行的次数也就非常多。因此若想看到明显的跟随效果,应该把 ratio 的值设得相对偏小。
同样是这段代码,我们可以试着把某些命令“//”(注释)掉,观察结果,从中理解程序的运行机制。
同时去掉 randomMove 和 follow 函数。粒子会维持初始状态,静止不动
去掉 randomMove 函数,只保留 follow 函数。粒子不会抖动
去掉 follow 函数,只保留 randomMove 函数。粒子在原地抖动
试着把 background 写在 setup 里,并将成员函数 draw 中的 alpha 值修改成 50。它就会变成一个特殊的笔刷工具
不同的透明度会产生不同的效果。去掉 randomMove 函数,只保留 follow 函数
类的综合应用-按钮
类除了能实现粒子系统,你还可以用它来做各种控件,例如按钮,滑动条。虽然不少插件中就有现成的,但自己手写控件有许多好处。一是可以从中熟悉类的用法,二是可以更灵活地定制需要的功能。当然,不用类也是可以写按钮的。但一个程序中如果需要用到多个按钮。不使用类就会非常麻烦,你必须重复声明变量和函数。而使用类就能做到一劳永逸,它相当于做了一个模子,需要的时候就用它生产零件即可。
代码示例(12-8):
Button btn;
void setup() {
size(700, 700);
btn = new Button(350, 600, 400, 40);
}
void draw() {
background(33, 48, 64);
btn.check();
btn.draw();
if (btn.active) {
for (int i = 0; i < 100; i++) {
noStroke();
fill(random(255), random(255), random(255),200);
float r = random(0, 400);
ellipse(350, 350, r, r);
}
} else {
fill(0);
ellipse(350, 350, 400, 400);
fill(50);
ellipse(350, 350, 360, 360);
}
}
void mousePressed() {
btn.mousePressed();
}
class Button {
float x, y, w, h; // 分别代表按钮中心位置的 x 坐标,y 坐标。按钮的长度,高度。
boolean over; // 检测鼠标是否在按钮上
boolean active; // 检测按钮是否被按下
Button(float x_, float y_, float w_, float h_) {
x = x_;
y = y_;
w = w_;
h = h_;
}
void check() {
if (mouseX > x - w/2 && mouseX < x + w/2 && mouseY > y - h/2 && mouseY < y + h/2) {
over = true;
} else {
over = false;
}
}
void mousePressed() {
if (over) {
active = !active;
}
}
void draw() {
if (over) {
fill(41, 238, 176);
} else {
fill(80);
}
rectMode(CENTER);
rect(x, y, w, h);
}
}
运行效果:
代码说明:
check 函数用于判断鼠标是否在按钮上。按钮由于是矩形,所以边界都可以通过计算得出
成员变量 active 用于记录按钮的激活状态。当鼠标在按钮上方并按下时,会对 active 的状态进行取反。以此达到切换效果
类的综合应用-滑动条
下面再提供一个有关滑动条的实例
代码示例(12-9):
Bar b;
void setup() {
size(700, 700);
b = new Bar(350, 600, 400, 15);
}
void draw() {
background(33, 48, 64);
b.draw();
fill(random(255), random(255), random(255));
noStroke();
float l = 400 * b.ratio;
ellipse(350, 350, l, l);
}
void mousePressed() {
b.mousePressed();
}
void mouseDragged() {
b.mouseDragged();
}
void mouseReleased() {
b.mouseReleased();
}
class Bar {
boolean isDrag; // 判断是否在拖动
boolean isActive; // 判断鼠标是否在控制点之上
float startX, endX; // 滑动条起始点与结束点的 x 坐标
float x, y; // 滑动条的 x,y 坐标
float w, r; // 滑动条的宽,控制点的半径
float circleX; // 控制点的 x 坐标
float ratio; // 比例
Bar(float x_, float y_, float w_, float r_) {
x = x_;
y = y_;
w = w_;
r = r_;
circleX = x;
startX = x - w/2;
endX = x + w/2;
}
void draw() {
if (isActive && isDrag) {
circleX = mouseX;
if (circleX > endX) {
circleX = endX;
} else if (circleX < startX) {
circleX = startX;
}
}
// 计算比率
ratio = (circleX - startX)/w;
// 绘制滑动条
stroke(130);
strokeWeight(4);
line(startX, y, endX, y);
if (isActive) {
fill(41, 238, 176);
} else {
fill(130);
}
noStroke();
ellipse(circleX, y, r * 2, r * 2);
}
void mousePressed() {
if (dist(mouseX, mouseY, circleX, y) < r) {
isActive = true;
} else {
isActive = false;
}
}
void mouseDragged() {
isDrag = true;
}
void mouseReleased() {
isDrag = false;
isActive = false;
}
}
运行效果:
代码说明:
由于滑动条的按钮为圆形,所以可以用距离来判断鼠标是否在按钮上方
ratio 变量代表滑动条的数值比例,它根据按钮坐标计算得出
END
以上介绍的知识点,仅仅是冰山一角。类还有很多的重要的特性和用法,诸如多态,继承。当你发现自己对上述用法已经非常娴熟了,同时也无法满足自己的需求,那就可以深入去学更多高级概念。技术不是掌握越多,钻得越深就越好,其实只要把基础规则理解透彻,有好的创意想法,也能做出足够有趣的作品。
随着学习越来越深入,会发现类是无处不在的。各种插件,各种库都是由类组成的。我们应该更有意识地去使用它,学会复用,学会抽象。
上面的粒子系统,还能做许多拓展,比如结合牛顿力学,将力,速度,加速度这些属性引入到类中。它可以模拟出更自然,更符合物理运动规律的粒子系统。由于这不是本系列的重点。也就不会详细展开。对此感兴趣的朋友可参考 Daniel Shiffman 的 《 The Nature of Code 》,中译本名为《代码本色》。关于力,里面有更详细的叙述。
与粒子系统的一些相关实例
引入力,增加更多粒子
引入力,用图片素材绘制粒子
在三维空间中使用粒子系统
下篇将会是整个系列上半部分的最终章,基础部分也接近尾声了。让我们一起走进 3D 的绘图世界~
Processing 系列文章
资源索引